/** @file
OHCI transfer scheduling routines.

Copyright (c) 2013-2015 Intel Corporation.

SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "Ohci.h"

/**

  Add an item of interrupt context

  @param  Ohc                   UHC private data
  @param  NewEntry              New entry to add

  @retval EFI_SUCCESS           Item successfully added

**/
EFI_STATUS
OhciAddInterruptContextEntry (
  IN  USB_OHCI_HC_DEV          *Ohc,
  IN  INTERRUPT_CONTEXT_ENTRY  *NewEntry
  )
{
  INTERRUPT_CONTEXT_ENTRY  *Entry;
  EFI_TPL                  OriginalTPL;

  OriginalTPL = gBS->RaiseTPL (TPL_NOTIFY);

  if (Ohc->InterruptContextList == NULL) {
    Ohc->InterruptContextList = NewEntry;
  } else {
    Entry = Ohc->InterruptContextList;
    while (Entry->NextEntry != NULL) {
      Entry = Entry->NextEntry;
    }

    Entry->NextEntry = NewEntry;
  }

  gBS->RestoreTPL (OriginalTPL);

  return EFI_SUCCESS;
}

/**

  Free a interrupt context entry

  @param  Ohc                   UHC private data
  @param  Entry                 Pointer to an interrupt context entry

  @retval EFI_SUCCESS           Entry freed
  @retval EFI_INVALID_PARAMETER Entry is NULL

**/
EFI_STATUS
OhciFreeInterruptContextEntry (
  IN USB_OHCI_HC_DEV          *Ohc,
  IN INTERRUPT_CONTEXT_ENTRY  *Entry
  )
{
  TD_DESCRIPTOR  *Td;

  if (Entry == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  if (Entry->UCBufferMapping != NULL) {
    DmaUnmap (Entry->UCBufferMapping);
  }

  if (Entry->UCBuffer != NULL) {
    UINTN  Pages = EFI_SIZE_TO_PAGES (Entry->DataLength);
    DmaFreeBuffer (Pages, Entry->UCBuffer);
  }

  while (Entry->DataTd) {
    Td            = Entry->DataTd;
    Entry->DataTd = (TD_DESCRIPTOR *)(UINTN)(Entry->DataTd->NextTDPointer);
    UsbHcFreeMem (Ohc->MemPool, Td, sizeof (TD_DESCRIPTOR));
  }

  FreePool (Entry);
  return EFI_SUCCESS;
}

/**

  Free entries match the device address and endpoint address

  @Param  Ohc                   UHC private date
  @Param  DeviceAddress         Item to free must match this device address
  @Param  EndPointAddress       Item to free must match this end point address
  @Param  DataToggle            DataToggle for output

  @retval  EFI_SUCCESS          Items match the requirement removed

**/
EFI_STATUS
OhciFreeInterruptContext (
  IN  USB_OHCI_HC_DEV  *Ohc,
  IN  UINT8            DeviceAddress,
  IN  UINT8            EndPointAddress,
  OUT UINT8            *DataToggle
  )
{
  INTERRUPT_CONTEXT_ENTRY  *Entry;
  INTERRUPT_CONTEXT_ENTRY  *TempEntry;
  EFI_TPL                  OriginalTPL;

  OriginalTPL = gBS->RaiseTPL (TPL_NOTIFY);

  while (Ohc->InterruptContextList != NULL &&
         Ohc->InterruptContextList->DeviceAddress == DeviceAddress &&
         Ohc->InterruptContextList->EndPointAddress == EndPointAddress)
  {
    TempEntry                 = Ohc->InterruptContextList;
    Ohc->InterruptContextList = Ohc->InterruptContextList->NextEntry;
    if (DataToggle != NULL) {
      *DataToggle = (UINT8)(TempEntry->DataTd->Word0.DataToggle & 0x1);
    }

    OhciFreeInterruptContextEntry (Ohc, TempEntry);
  }

  Entry = Ohc->InterruptContextList;
  if (Entry == NULL) {
    gBS->RestoreTPL (OriginalTPL);
    return EFI_SUCCESS;
  }

  while (Entry->NextEntry != NULL) {
    if ((Entry->NextEntry->DeviceAddress == DeviceAddress) &&
        (Entry->NextEntry->EndPointAddress == EndPointAddress))
    {
      TempEntry        = Entry->NextEntry;
      Entry->NextEntry = Entry->NextEntry->NextEntry;
      if (DataToggle != NULL) {
        *DataToggle = (UINT8)(TempEntry->DataTd->Word0.DataToggle & 0x1);
      }

      OhciFreeInterruptContextEntry (Ohc, TempEntry);
    } else {
      Entry = Entry->NextEntry;
    }
  }

  gBS->RestoreTPL (OriginalTPL);

  return EFI_SUCCESS;
}

/**

  Convert Error code from OHCI format to EFI format

  @Param  ErrorCode             ErrorCode in OHCI format

  @retval                       ErrorCode in EFI format

**/
UINT32
ConvertErrorCode (
  IN  UINT32  ErrorCode
  )
{
  UINT32  TransferResult;

  switch (ErrorCode) {
    case TD_NO_ERROR:
      TransferResult = EFI_USB_NOERROR;
      break;

    case TD_TOBE_PROCESSED:
    case TD_TOBE_PROCESSED_2:
      TransferResult = EFI_USB_ERR_NOTEXECUTE;
      break;

    case TD_DEVICE_STALL:
      TransferResult = EFI_USB_ERR_STALL;
      break;

    case TD_BUFFER_OVERRUN:
    case TD_BUFFER_UNDERRUN:
      TransferResult = EFI_USB_ERR_BUFFER;
      break;

    case TD_CRC_ERROR:
      TransferResult = EFI_USB_ERR_CRC;
      break;

    case TD_NO_RESPONSE:
      TransferResult = EFI_USB_ERR_TIMEOUT;
      break;

    case TD_BITSTUFFING_ERROR:
      TransferResult = EFI_USB_ERR_BITSTUFF;
      break;

    default:
      TransferResult = EFI_USB_ERR_SYSTEM;
  }

  return TransferResult;
}

/**

  Check TDs Results

  @Param  Ohc                   UHC private data
  @Param  Td                    TD_DESCRIPTOR
  @Param  Result                Result to return

  @retval TRUE                  means OK
  @retval FLASE                 means Error or Short packet

**/
BOOLEAN
OhciCheckTDsResults (
  IN  USB_OHCI_HC_DEV  *Ohc,
  IN  TD_DESCRIPTOR    *Td,
  OUT UINT32           *Result
  )
{
  UINT32  TdCompletionCode;

  *Result = EFI_USB_NOERROR;

  while (Td) {
    TdCompletionCode = Td->Word0.ConditionCode;

    *Result |= ConvertErrorCode (TdCompletionCode);
    //
    // if any error encountered, stop processing the left TDs.
    //
    if (*Result) {
      return FALSE;
    }

    Td = (TD_DESCRIPTOR *)(UINTN)(Td->NextTDPointer);
  }

  return TRUE;
}

/**

  Check the task status on an ED

  @Param  Ed                    Pointer to the ED task that TD hooked on
  @Param  HeadTd                TD header for current transaction

  @retval                       Task Status Code

**/
UINT32
CheckEDStatus (
  IN  ED_DESCRIPTOR   *Ed,
  IN  TD_DESCRIPTOR   *HeadTd,
  OUT OHCI_ED_RESULT  *EdResult
  )
{
  while (HeadTd != NULL) {
    if (HeadTd->NextTDPointer == 0) {
      return TD_NO_ERROR;
    }

    if (HeadTd->Word0.ConditionCode != 0) {
      return HeadTd->Word0.ConditionCode;
    }

    EdResult->NextToggle = ((UINT8)(HeadTd->Word0.DataToggle) & BIT0) ^ BIT0;
    HeadTd               = (TD_DESCRIPTOR *)(UINTN)(HeadTd->NextTDPointer);
  }

  if (OhciGetEDField (Ed, ED_TDHEAD_PTR) != OhciGetEDField (Ed, ED_TDTAIL_PTR)) {
    return TD_TOBE_PROCESSED;
  }

  return TD_NO_ERROR;
}

/**

  Check the task status

  @Param  Ohc                   UHC private data
  @Param  ListType              Pipe type
  @Param  Ed                    Pointer to the ED task hooked on
  @Param  HeadTd                Head of TD corresponding to the task
  @Param  ErrorCode             return the ErrorCode

  @retval  EFI_SUCCESS          Task done
  @retval  EFI_NOT_READY        Task on processing
  @retval  EFI_DEVICE_ERROR     Some error occured

**/
EFI_STATUS
CheckIfDone (
  IN  USB_OHCI_HC_DEV       *Ohc,
  IN  DESCRIPTOR_LIST_TYPE  ListType,
  IN  ED_DESCRIPTOR         *Ed,
  IN  TD_DESCRIPTOR         *HeadTd,
  OUT OHCI_ED_RESULT        *EdResult
  )
{
  EdResult->ErrorCode = TD_TOBE_PROCESSED;

  switch (ListType) {
    case CONTROL_LIST:
      if (OhciGetHcCommandStatus (Ohc, CONTROL_LIST_FILLED) != 0) {
        return EFI_NOT_READY;
      }

      break;
    case BULK_LIST:
      if (OhciGetHcCommandStatus (Ohc, BULK_LIST_FILLED) != 0) {
        return EFI_NOT_READY;
      }

      break;
    default:
      break;
  }

  EdResult->ErrorCode = CheckEDStatus (Ed, HeadTd, EdResult);

  if (EdResult->ErrorCode == TD_NO_ERROR) {
    return EFI_SUCCESS;
  } else if (EdResult->ErrorCode == TD_TOBE_PROCESSED) {
    return EFI_NOT_READY;
  } else {
    return EFI_DEVICE_ERROR;
  }
}

/**

  Convert TD condition code to Efi Status

  @Param  ConditionCode         Condition code to convert

  @retval  EFI_SUCCESS          No error occured
  @retval  EFI_NOT_READY        TD still on processing
  @retval  EFI_DEVICE_ERROR     Error occured in processing TD

**/
EFI_STATUS
OhciTDConditionCodeToStatus (
  IN  UINT32  ConditionCode
  )
{
  if (ConditionCode == TD_NO_ERROR) {
    return EFI_SUCCESS;
  }

  if (ConditionCode == TD_TOBE_PROCESSED) {
    return EFI_NOT_READY;
  }

  return EFI_DEVICE_ERROR;
}

/**

  Invoke callbacks hooked on done TDs

  @Param  Entry                 Interrupt transfer transaction information data structure
  @Param  Context               Ohc private data

**/
VOID
OhciInvokeInterruptCallBack (
  IN  INTERRUPT_CONTEXT_ENTRY  *Entry,
  IN  UINT32                   Result
  )
{
  // Generally speaking, Keyboard driver should not
  // check the Keyboard buffer if an error happens, it will be robust
  // if we NULLed the buffer once error happens
  if (Result) {
    Entry->CallBackFunction (
             NULL,
             0,
             Entry->Context,
             Result
             );
  } else {
    Entry->CallBackFunction (
             (VOID *)(UINTN)(Entry->DataTd->DataBuffer),
             Entry->DataTd->ActualSendLength,
             Entry->Context,
             Result
             );
  }
}

/**

  Timer to submit periodic interrupt transfer, and invoke callbacks hooked on done TDs

  @param  Event                 Event handle
  @param  Context               Device private data

**/
VOID
EFIAPI
OhciHouseKeeper (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  USB_OHCI_HC_DEV          *Ohc;
  INTERRUPT_CONTEXT_ENTRY  *Entry;
  INTERRUPT_CONTEXT_ENTRY  *PreEntry;
  ED_DESCRIPTOR            *Ed;
  TD_DESCRIPTOR            *DataTd;
  TD_DESCRIPTOR            *HeadTd;

  UINT8    Toggle;
  EFI_TPL  OriginalTPL;
  UINT32   Result;

  Ohc         = (USB_OHCI_HC_DEV *)Context;
  OriginalTPL = gBS->RaiseTPL (TPL_NOTIFY);

  Entry    = Ohc->InterruptContextList;
  PreEntry = NULL;

  while (Entry != NULL) {
    OhciCheckTDsResults (Ohc, Entry->DataTd, &Result);
    if (((Result & EFI_USB_ERR_STALL) == EFI_USB_ERR_STALL) ||
        ((Result & EFI_USB_ERR_NOTEXECUTE) == EFI_USB_ERR_NOTEXECUTE))
    {
      PreEntry = Entry;
      Entry    = Entry->NextEntry;
      continue;
    }

    if (Entry->CallBackFunction != NULL) {
      OhciInvokeInterruptCallBack (Entry, Result);
      if (Ohc->InterruptContextList == NULL) {
        gBS->RestoreTPL (OriginalTPL);
        return;
      }
    }

    if (Entry->IsPeriodic) {
      Ed     = Entry->Ed;
      HeadTd = Entry->DataTd;
      DataTd = HeadTd;
      Toggle = 0;
      if (Result == EFI_USB_NOERROR) {
        //
        // Update toggle if there is no error, and re-submit the interrupt Ed&Tds
        //
        if ((Ed != NULL) && (DataTd != NULL)) {
          Ed->Word0.Skip = 1;
        }

        //
        // From hcir1_0a.pdf 4.2.2
        // ToggleCarry:This bit is the data toggle carry bit,
        // Whenever a TD is retired, this bit is written to
        // contain the last data toggle value(LSb of data Toggel
        // file) from the retired TD.
        // This field is not used for Isochronous Endpoints
        //
        if (Ed == NULL) {
          return;
        }

        Toggle = (UINT8)OhciGetEDField (Ed, ED_DTTOGGLE);
        while (DataTd != NULL) {
          if (DataTd->NextTDPointer == 0) {
            DataTd->Word0.DataToggle = 0;
            break;
          } else {
            OhciSetTDField (DataTd, TD_DT_TOGGLE, Toggle);
          }

          DataTd  = (TD_DESCRIPTOR *)(UINTN)(DataTd->NextTDPointer);
          Toggle ^= 1;
        }

        //
        // HC will only update DataToggle, ErrorCount, ConditionCode
        // CurrentBufferPointer & NextTD, so we only need to update
        // them once we want to active them again
        //
        DataTd = HeadTd;
        while (DataTd != NULL) {
          if (DataTd->NextTDPointer == 0) {
            OhciSetTDField (DataTd, TD_ERROR_CNT | TD_COND_CODE | TD_CURR_BUFFER_PTR | TD_NEXT_PTR, 0);
            break;
          }

          OhciSetTDField (DataTd, TD_ERROR_CNT, 0);
          OhciSetTDField (DataTd, TD_COND_CODE, TD_TOBE_PROCESSED);
          DataTd->NextTD            = DataTd->NextTDPointer;
          DataTd->CurrBufferPointer = DataTd->DataBuffer;
          DataTd                    = (TD_DESCRIPTOR *)(UINTN)(DataTd->NextTDPointer);
        }

        //
        // Active current Ed,Td
        //
        // HC will only update Halted, ToggleCarry & TDQueueHeadPointer,
        // So we only need to update them once we want to active them again.
        //
        if ((Ed != NULL) && (DataTd != NULL)) {
          Ed->Word2.TdHeadPointer = (UINT32)((UINTN)HeadTd>>4);
          OhciSetEDField (Ed, ED_HALTED | ED_DTTOGGLE, 0);
          Ed->Word0.Skip = 0;
        }
      } else {
        return;
      }
    } else {
      if (PreEntry == NULL) {
        Ohc->InterruptContextList = Entry->NextEntry;
      } else {
        PreEntry            = Entry;
        PreEntry->NextEntry = Entry->NextEntry;
      }

      OhciFreeInterruptContextEntry (Ohc, PreEntry);
      gBS->RestoreTPL (OriginalTPL);
      return;
    }

    PreEntry = Entry;
    Entry    = Entry->NextEntry;
  }

  gBS->RestoreTPL (OriginalTPL);
}
